<?php

namespace app\common\utils;

/**
 * IP工具类
 */
class IpUtils
{
    const REGX_0_255 = '(25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)';
    const REGX_IP = '((' . self::REGX_0_255 . '\.){3}' . self::REGX_0_255 . ')';
    const REGX_IP_WILDCARD = '(((\*\.){3}\*)|(' . self::REGX_0_255 . '(\.\*){3})|(' . self::REGX_0_255 . '\.' . self::REGX_0_255 . ')(\.\*){2}' . '|((' . self::REGX_0_255 . '\.){3}\*))';
    const REGX_IP_SEG = '(' . self::REGX_IP . '\-' . self::REGX_IP . ')';

    /**
     * 获取客户端IP
     * @return string
     */
    public static function getIpAddr(): string
    {
        return self::getClientIp();
    }

    /**
     * 获取客户端IP
     * @return string
     */
    public static function getClientIp(): string
    {
        $request = request();
        $ip = $request->header('x-forwarded-for');
        if (empty($ip) || strtolower($ip) == 'unknown') {
            $ip = $request->header('proxy-client-ip');
        }
        if (empty($ip) || strtolower($ip) == 'unknown') {
            $ip = $request->header('x-forwarded-for');
        }
        if (empty($ip) || strtolower($ip) == 'unknown') {
            $ip = $request->header('wl-proxy-client-ip');
        }
        if (empty($ip) || strtolower($ip) == 'unknown') {
            $ip = $request->header('x-real-ip');
        }
        if (empty($ip) || strtolower($ip) == 'unknown') {
            $ip = $request->ip();
        }

        return $ip == '::1' ? '127.0.0.1' : self::getMultistageReverseProxyIp($ip);
    }

    /**
     * 检查是否为内部IP地址
     * @param string $ip
     * @return bool
     */
    public static function internalIp(string $ip): bool
    {
        $addr = self::textToNumericFormatV4($ip);
        return self::internalIpByBytes($addr) || $ip == '127.0.0.1';
    }

    /**
     * 检查是否为内部IP地址
     * @param array|null $addr
     * @return bool
     */
    private static function internalIpByBytes(?array $addr): bool
    {
        if (empty($addr) || count($addr) < 2) {
            return true;
        }

        $b0 = $addr[0];
        $b1 = $addr[1];

        // 10.x.x.x/8
        $section1 = 0x0A;
        // 172.16.x.x/12
        $section2 = 0xAC;
        $section3 = 0x10;
        $section4 = 0x1F;
        // 192.168.x.x/16
        $section5 = 0xC0;
        $section6 = 0xA8;

        switch ($b0) {
            case $section1:
                return true;
            case $section2:
                if ($b1 >= $section3 && $b1 <= $section4) {
                    return true;
                }
                break;
            case $section5:
                switch ($b1) {
                    case $section6:
                        return true;
                }
                break;
        }

        return false;
    }

    /**
     * 将IPv4地址转换为字节数组
     * @param string $text
     * @return array|null
     */
    public static function textToNumericFormatV4(string $text): ?array
    {
        if (empty($text)) {
            return null;
        }

        $bytes = array_fill(0, 4, 0);
        $elements = explode('.', $text);

        try {
            switch (count($elements)) {
                case 1:
                    $l = intval($elements[0]);
                    if ($l < 0 || $l > 4294967295) {
                        return null;
                    }
                    $bytes[0] = ($l >> 24) & 0xFF;
                    $bytes[1] = ($l >> 16) & 0xFF;
                    $bytes[2] = ($l >> 8) & 0xFF;
                    $bytes[3] = $l & 0xFF;
                    break;
                case 2:
                    $l = intval($elements[0]);
                    if ($l < 0 || $l > 255) {
                        return null;
                    }
                    $bytes[0] = $l;
                    $l = intval($elements[1]);
                    if ($l < 0 || $l > 16777215) {
                        return null;
                    }
                    $bytes[1] = ($l >> 16) & 0xFF;
                    $bytes[2] = ($l >> 8) & 0xFF;
                    $bytes[3] = $l & 0xFF;
                    break;
                case 3:
                    for ($i = 0; $i < 2; $i++) {
                        $l = intval($elements[$i]);
                        if ($l < 0 || $l > 255) {
                            return null;
                        }
                        $bytes[$i] = $l;
                    }
                    $l = intval($elements[2]);
                    if ($l < 0 || $l > 65535) {
                        return null;
                    }
                    $bytes[2] = ($l >> 8) & 0xFF;
                    $bytes[3] = $l & 0xFF;
                    break;
                case 4:
                    for ($i = 0; $i < 4; $i++) {
                        $l = intval($elements[$i]);
                        if ($l < 0 || $l > 255) {
                            return null;
                        }
                        $bytes[$i] = $l;
                    }
                    break;
                default:
                    return null;
            }
        } catch (\Exception $e) {
            return null;
        }

        return $bytes;
    }

    /**
     * 获取服务器IP地址
     * @return string
     */
    public static function getHostIp(): string
    {
        try {
            return gethostbyname(gethostname());
        } catch (\Exception $e) {
            return '127.0.0.1';
        }
    }

    /**
     * 获取服务器主机名
     * @return string
     */
    public static function getHostName(): string
    {
        try {
            return gethostname();
        } catch (\Exception $e) {
            return '未知';
        }
    }

    /**
     * 从多级反向代理中获得第一个非unknown IP地址
     * @param string $ip
     * @return string
     */
    public static function getMultistageReverseProxyIp(string $ip): string
    {
        if (strpos($ip, ',') !== false) {
            $ips = explode(',', trim($ip));
            foreach ($ips as $subIp) {
                if (!self::isUnknown($subIp)) {
                    $ip = $subIp;
                    break;
                }
            }
        }
        return substr($ip, 0, 255);
    }

    /**
     * 检测给定字符串是否为unknown
     * @param string $checkString
     * @return bool
     */
    public static function isUnknown(string $checkString): bool
    {
        return empty($checkString) || strtolower($checkString) == 'unknown';
    }

    /**
     * 是否为IP
     * @param string $ip
     * @return bool
     */
    public static function isIP(string $ip): bool
    {
        return !empty($ip) && preg_match('/^' . self::REGX_IP . '$/', $ip);
    }

    /**
     * 是否为IP或通配符地址
     * @param string $ip
     * @return bool
     */
    public static function isIpWildCard(string $ip): bool
    {
        return !empty($ip) && preg_match('/^' . self::REGX_IP_WILDCARD . '$/', $ip);
    }

    /**
     * 检测IP是否在通配符中
     * @param string $ipWildCard
     * @param string $ip
     * @return bool
     */
    public static function ipIsInWildCardNoCheck(string $ipWildCard, string $ip): bool
    {
        $s1 = explode('.', $ipWildCard);
        $s2 = explode('.', $ip);
        $isMatchedSeg = true;

        for ($i = 0; $i < count($s1) && $s1[$i] != '*'; $i++) {
            if ($s1[$i] != $s2[$i]) {
                $isMatchedSeg = false;
                break;
            }
        }

        return $isMatchedSeg;
    }

    /**
     * 是否为IP段
     * @param string $ipSeg
     * @return bool
     */
    public static function isIPSegment(string $ipSeg): bool
    {
        return !empty($ipSeg) && preg_match('/^' . self::REGX_IP_SEG . '$/', $ipSeg);
    }

    /**
     * 判断IP是否在指定网段中
     * @param string $iparea
     * @param string $ip
     * @return bool
     */
    public static function ipIsInNetNoCheck(string $iparea, string $ip): bool
    {
        $idx = strpos($iparea, '-');
        $sips = explode('.', substr($iparea, 0, $idx));
        $sipe = explode('.', substr($iparea, $idx + 1));
        $sipt = explode('.', $ip);

        $ips = 0;
        $ipe = 0;
        $ipt = 0;
        for ($i = 0; $i < 4; $i++) {
            $ips = ($ips << 8) | intval($sips[$i]);
            $ipe = ($ipe << 8) | intval($sipe[$i]);
            $ipt = ($ipt << 8) | intval($sipt[$i]);
        }

        if ($ips > $ipe) {
            $t = $ips;
            $ips = $ipe;
            $ipe = $t;
        }

        return $ips <= $ipt && $ipt <= $ipe;
    }

    /**
     * 校验IP是否符合过滤规则
     * @param string $filter
     * @param string $ip
     * @return bool
     */
    public static function isMatchedIp(string $filter, string $ip): bool
    {
        if (empty($filter) || empty($ip)) {
            return false;
        }

        $ips = explode(';', $filter);
        foreach ($ips as $iStr) {
            if (self::isIP($iStr) && $iStr == $ip) {
                return true;
            } elseif (self::isIpWildCard($iStr) && self::ipIsInWildCardNoCheck($iStr, $ip)) {
                return true;
            } elseif (self::isIPSegment($iStr) && self::ipIsInNetNoCheck($iStr, $ip)) {
                return true;
            }
        }

        return false;
    }
}